
jsal.module({
    name : 'heist',
	depends : ['heist.runtime'],
    onload : function () {
		if (! this.heist) this.heist = {};
		var heist = this.heist;

		heist.ast = {};

        if (! Array.prototype.map) {
            Array.prototype.map = function (callback) {
                if (typeof(callback) != "function")
                    throw "parameter to map must be a function";
                var len = this.length;
                var res = new Array(len);
                var context = arguments[1] || glob;
                for (var i = 0; i < len; i++) {
                    if (i in this) res[i] = callback.apply(context, [this[i], i, this]);
                }
                return res;
            };
        }

        heist.quote = function (str) {
            if (typeof str == 'undefined') return 'undefined';
            return ('"' + str.replace(/(?:(\r\n|\n|\r)|([\\\"]))/g, function (lineBreak, character) {
                return lineBreak ? "\\n\"+\n\"" : "\\" + character;
            }) + '"').replace(/\+(?:\r\n|\n|\r)\"\"$/, "\n");
        };

        heist.htmlEscape = function (str) {
            if (str == undefined) return '';
            return str.replace(/([&\"<>])/g, function (character) {
                return '&' + (
                    character == '&' ? 'amp' :
                    character == '"' ? 'quot' :
                    character == '<' ? 'lt' :
                        'gt'
                ) + ';';
            });
        };

        heist.ast.Method = function (
            preIdentifierWhitespace,
            methodName,
            preArgumentListWhitespace,
            argumentList,
            preTagCloseWhitespace,
            subnodes
        ) {
            this.type = 'Method';
            this.preIdentifierWhitespace = preIdentifierWhitespace;
            this.methodName = methodName;
            this.preArgumentListWhitespace = preArgumentListWhitespace;
            this.argumentList = argumentList == "" ? undefined : argumentList;
            this.preTagCloseWhitespace = preTagCloseWhitespace;

            this.subnodes = subnodes || [];
            this.lastSubnode = function () {
                return this.subnodes[this.subnodes.length - 1];
            };
        };

        heist.ast.Root = function (root, path, subnodes) {
            this.type = 'Root';
            this.root = root;
            this.path = path;
            this.subnodes = subnodes || [];
            this.lastSubnode = function () {
                return this.subnodes[this.subnodes.length - 1];
            };
        };

		heist.ast.Root.prototype.methods = function () {
			var methods = [];
			for (var i = 0, l = this.subnodes.length; i < l; i++) {
				if (this.subnodes[i] instanceof heist.ast.Method)
					methods.push(this.subnodes[i]);
			}
			return methods;
		}

		heist.ast.Root.prototype.contents = function () {
			var contents = [];
			for (var i = 0, l = this.subnodes.length; i < l; i++) {
				if (! (this.subnodes[i] instanceof heist.ast.Method))
					contents.push(this.subnodes[i]);
			}
			return contents;
		}

        heist.ast.ContainingCode = function (code) {
            this.type = 'ContainingCode';
            this.code = code;    
        };

        heist.ast.Content = function (text) {
            this.type = 'Content';
            this.text = text;
        };

        heist.ast.CodeLines = function (code) {
            this.type = 'CodeLines';
            this.code = code;
        };

        heist.ast.ExpressionTag = function (code, filters) {
            this.type = 'ExpressionTag';
            this.code = code;
            this.filters = filters;
        };

        heist.ast.CodeTag = function (code) {
            this.type = 'CodeTag';
            this.code = code;
        };

        heist.ast.OutputStart = function () {
            this.type = 'OutputStart';
        };

        heist.ast.OutputStop = function () {
            this.type = 'OutputStop';
        };

        heist.ast.IgnoredContent = function (text) {
            this.type = 'IgnoredContent';
            this.text = text;
        };

		heist.ast.IgnoredContent.prototype.newlines = function () {
			return this.text.replace(/[^\r\n\f]/g, '');
		};

        if (! heist.generate) heist.generate = {};

        heist.generate.addGenerator = function (name, proto) {
            var Renderer = function () {
                this.content = '';
            };
            Renderer.prototype = proto;
            heist.generate[name] = function (ast) {
                var renderer = new Renderer();
                renderer.Root(ast);
                return renderer.content;
            };
        };

        heist.generate.dispatch = function (nodes, separator) {
            for (var i = 0, len = nodes.length; i < len; i++) {
                var gen = this[nodes[i].type];
                if (! gen) throw 'no generator method for ' + nodes[i].type;
                if (separator && i != 0) this.content += separator;
                gen.apply(this, [nodes[i]]);
            }
        };

        heist.generate.addGenerator('javascript', {
            Root: function (node) {
				this.content += 'function(comp){comp.addDefaultMethod(function(){';
                heist.generate.dispatch.call(this, node.contents());
				this.content += '});';
				heist.generate.dispatch.call(this, node.methods());
                this.content += '}';
            },
            ContainingCode: function (node) {
                this.content += node.code;    
            },
            Method: function (node) {
                this.content += 'comp.addMethod(';
                this.content += node.preIdentifierWhitespace;
                this.content += heist.quote(node.methodName) + ',function';
                this.content += node.preArgumentListWhitespace;
                this.content += node.argumentList || '()';
                this.content += node.preTagCloseWhitespace;
                this.content += '{';
                heist.generate.dispatch.apply(this, [node.subnodes]);
                this.content += '});';
            },
            Content: function (node) {
                this.content += 'this.append(' + heist.quote(node.text) + ');';
            },
			IgnoredContent: function (node) {
				this.content += node.newlines();
			},
            CodeLines: function (node) {
                this.content += node.code;
            },
            ExpressionTag: function (node) {
                this.content += 'this.append(' + node.code + ');';
            },
            CodeTag: function (node) {
                this.content += node.code + ';';
            },
			OutputStop: function () {},
			OutputStart: function () {}
        });

        heist.generate.addGenerator('ast', {
            Root: function (node) {
                this.indent = '';
                this.content += 'new heist.ast.Root(' +
                    heist.quote(node.root) + ',' +
                    heist.quote(node.path) + ',';
                heist.generate.dispatch.apply(this, [node.subnodes, ', ']);
                this.content += ')';
            },
            ContainingCode: function (node) {
                this.content += 'new heist.ast.ContainingCode(';
                this.content += heist.quote(node.code);
                this.content += ')';
            },
            Method: function (node) {
                this.indent += '    ';
                this.content += 'new heist.ast.Method(';
                this.content += (
					[   node.preIdentifierWhitespace,
                        node.methodName,
                        node.preArgumentListWhitespace,
                        node.argumentList,
                        node.preTagCloseWhitespace
                    ].map(function (str) { return heist.quote(str); }).join(', '));
                this.content += ', [';
                heist.generate.dispatch.apply(this, [node.subnodes, ', ']);
                this.content += '])';
            },
            Content: function (node) {
                this.content += 'new heist.ast.Content(' + heist.quote(node.text) + ')';
            },
			IgnoredContent: function (node) {
				this.content += 'new heist.ast.IgnoredContent(' + heist.quote(node.text) + ')';
			},
            CodeLines: function (node) {
                this.content += 'new heist.ast.CodeLines(' + heist.quote(node.code) + ')';
            },
            ExpressionTag: function (node) {
                this.content += 'new heist.ast.ExpressionTag(' + heist.quote(node.code) + ')';
            },
            CodeTag: function (node) {
                this.content += 'new heist.ast.CodeTag(' + heist.quote(node.code) + ')';
            },
			OutputStop: function () {
                this.content += 'new heist.ast.OutputStop()';
			},
			OutputStart: function () {
                this.content += 'new heist.ast.OutputStart()';
			}
        });

        heist.generate.addGenerator('html', {
            Root: function (node) {
                this.content += '<pre>';
                heist.generate.dispatch.apply(this, [node.subnodes]);
                this.content += '</pre>';
            },
            ContainingCode: function (node) {
                this.content += '<span class="containingcode">';
                this.content += heist.htmlEscape(node.code);
                this.content += '</span>';
            },
            Method: function (node) {
                this.content += '<span class="method-start">&lt;~</span>';
                this.content += node.preIdentifierWhitespace;
                this.content += '<span class="method-name">' + heist.htmlEscape(node.methodName) + '</span>';
                this.content += node.preArgumentListWhitespace;
                this.content += '<span class="method-arglist">' + heist.htmlEscape(node.argumentList) + '</span>';
                this.content += node.preTagCloseWhitespace;
                this.content += '<span class="method-end">&gt;</span>';
                heist.generate.dispatch.apply(this, [node.subnodes]);
                this.content += '<span class="method-closing">&lt;/~&gt;</span>';
            },
            Content: function (node) {
                this.content += heist.htmlEscape(node.text);
            },
			IgnoredContent: function (node) {
				this.content += '<span class="ignored-content">' + heist.htmlEscape(node.text) + '</span>';
			},
            CodeLines: function (node) {
                this.content += '<span class="codeline-start">:</span><span class="codeline-code">' + heist.htmlEscape(node.code) + '</span>';
            },
            ExpressionTag: function (node) {
                this.content += '<span class="expressiontag-start">&lt;(</span><span class="expressiontag-code">' + heist.htmlEscape(node.code) + '</span><span class="expressiontag-stop">)&gt;</span>';
            },
            CodeTag: function (node) {
                this.content += '<span class="codetag-start">&lt;[</span><span class="codetag-code">' + node.code + '</span><span class="codetag-stop">]&gt;</span>';
            },
			OutputStop: function () {
                this.content += '<span class="output-stop">|-</span>';
			},
			OutputStart: function () {
                this.content += '<span class="output-start">-|</span>';
			}
        });


        heist.Parser = function(){
            
            var Parser = function (opts) {
				this.embeddedInCode = opts.embeddedInCode;
				this.defaultOutputOn = opts.defaultOutputOn;
			};
            
            var i = 0;
            var topLevelContext            = i++;
            var topLevelInCodeContext      = i++;
            var inMethodContext            = i++;
            var directiveContext           = i++;
            var methodContext              = i++;
            var methodCloseContext         = i++;
            var codeTagStartContext        = i++;
            var expressionTagStartContext  = i++;
            var codeLineContext            = i++;
            var suppressedLineBreakContext = i++;
            var outputStartContext         = i++;
            var outputStopContext          = i++;
            var endOfInput                 = i++;

            Parser.prototype = {
				comp : function (content) {
					return heist.runtime.compFromJs(
						heist.generate.javascript(this.parseAst(content))
					);
				},
                parseAst: function (content, path, root) {
                    var ast = heist.ast;
                    var position;
                    var newPosition;
                    var tokenStart = 0;
                    
                    var root = new ast.Root(root, path);
                    var stack = [root];
                    var res;
                    var outputOn = this.defaultOutputOn;
                    var methodNode;

                    var context = topLevelContext;

                    if (content.charAt(0) == ':') {
                        newPosition = 1;
                        context = codeLineContext;
                    }

                    var codeLineFollowingSuppressedLineBreak;
                    
                    var topLevel = /([\S\s]*?)(?:(\\\n\:?)|(\n\:)|(\<\~)(\=?)|(\<\()|(\<\[)|(\|\-)|(\-\|)|(\<\/\~)|$)/g;
					var topLevelInCode = /([\S\s]*?)(?:(\<\~)(\=?)|$)/g;
                    var inMethod = /([\S\s]*?)(?:(\\\n\:?)|(\n\:)|(\<\()|(\<\[)|(\|\-)|(\-\|)|(\<\~(?!\\=))|(\<\/\~)|$)/g;
                    var directive = /(\s*)(\w+)(\s+)([^>\s]+?)(\s*)\>/g;
                    var method = /(\s*)([a-zA-Z$][\w$]*)(\s*)((?:\([^\)]*\))?)(\s*)\>/g;
                    var methodClose = /(\s*)((?:[a-zA-Z$][\w$]*)?)(\s*)\>/g;
                    var codeTagStart = /([\S\s]*?)\]\>/g;
                    var expressionTagStart = /([\S\s]*?)\)((?:\w+(?:\,\w+)*)?)\>/g;
                    var codeLine = /(.*?(?:\n|$))/g;
					
					var extraNewline;

                    try {
                        
                    SUCCESS:
                        while (true) {
                            position = newPosition || 0;

                            if (false) {
								var debugMethod = print || alert;
                                debugMethod("--- parsing from " + position + " with context: " + context + "\n" +
                                            "        - stack has " + stack.length + " item(s)\n" +
                                            "        - " + stack.join(','));
                            }

                            switch (context) {

                            case topLevelContext:
                                topLevel.lastIndex = position;
                                res = topLevel.exec(content);
                                if (res == null) throw 'could not match template';
                                newPosition = topLevel.lastIndex;

                                extraNewline = res[3] && res[3].length == 2 ? "\n" : "";
                                if (res[1] || extraNewline) {
                                    contentWithNewline = res[1] + extraNewline;
                                    if (outputOn)
                                        stack[stack.length - 1].subnodes.push(new ast.Content(contentWithNewline));
                                    else
                                        stack[stack.length - 1].subnodes.push(new ast.IgnoredContent(contentWithNewline));
                                }

                                if (res[2]) { 
                                    context = suppressedLineBreakContext;
                                    codeLineFollowingSuppressedLineBreak = (res[2].length == 3);
                                }
                                else if (res[3]) {
                                    tokenStart = newPosition - 1;
                                    context = codeLineContext;
                                }
								else if (res[5]) {
                                    tokenStart = newPosition - 3;
                                    context = directiveContext;
                                }
                                else if (res[4]) {
                                    tokenStart = newPosition - 2;
                                    context = methodContext;
                                }
                                else if (res[6]) {
                                    tokenStart = newPosition - 2;
                                    context = expressionTagStartContext;
                                }
                                else if (res[7]) {
                                    tokenStart = newPosition - 2;
                                    context = codeTagStartContext;
                                }
                                else if (res[8]) {
                                    tokenStart = newPosition - 2;
                                    context = outputStopContext;
                                }
                                else if (res[9]) {
                                    tokenStart = newPosition - 2;
                                    context = outputStartContext;
                                }
                                else if (res[10]) {
                                    tokenStart = newPosition - 3;
                                    context = methodCloseContext;
                                }
                                else {
                                    context = endOfInput;
                                }
                                break;                        

                            case topLevelInCodeContext:
                                
                                topLevelInCode.lastIndex = position;
                                res = topLevelInCode.exec(content);
                                if (res == null) throw 'could not match template';
                                newPosition = topLevelInCode.lastIndex;
                                if (res[1])
                                    stack[stack.length - 1].subnodes.push(
										new ast.ContainingCode(res[1])
									);
                                
                                if (res[3]) {
                                    tokenStart = newPosition - 3;
                                    context = directiveContext;
                                }
                                else if (res[2]) {
                                    tokenStart = newPosition - 2;
                                    context = methodContext;
                                }
                                else {
                                    context = endOfInput;
                                }    
                                break;
                                
                            case inMethodContext:
                                inMethod.lastIndex = position;
                                res = inMethod.exec(content);
                                if (res == null) throw 'could not match template';
                                newPosition = inMethod.lastIndex;
                                extraNewline = res[3] && res[3].length == 2 ? "\n" : "";
                                if (res[1] || extraNewline) {
                                    contentWithNewline = res[1] + extraNewline;
                                    if (outputOn)
                                        stack[stack.length - 1].subnodes.push(new ast.Content(contentWithNewline));
                                    else
                                        stack[stack.length - 1].subnodes.push(new ast.IgnoredContent(contentWithNewline));
                                }
                                
                                if (res[2]) {
                                    context = suppressedLineBreakContext;
                                    codeLineFollowingSuppressedLineBreak = (res[2].length == 3);
                                }
                                else if (res[3]) {
                                    tokenStart = newPosition - 1;
                                    context = codeLineContext;
                                }
                                else if (res[4]) {
                                    tokenStart = newPosition - 2;
                                    context = expressionTagStartContext;
                                }
                                else if (res[5]) {
                                    tokenStart = newPosition - 2;
                                    context = codeTagStartContext;
                                }
                                else if (res[6]) {
                                    tokenStart = newPosition - 2;
                                    context = outputStopContext;
                                }
                                else if (res[7]) {
                                    tokenStart = newPosition - 2;
                                    context = outputStartContext;
                                }
                                else if (res[8]) {
                                    tokenStart = newPosition - 2;
                                    context = methodContext;
                                }
                                else if (res[9]) {
                                    tokenStart = newPosition - 3;
                                    context = methodCloseContext;
                                }
                                else {
                                    context = endOfInput;
                                }
                                break;                        

                            case directiveContext:

                                directive.lastIndex = position;
                                res = directive.exec(content);
                                if (res == null) throw 'invalid directive';
                                newPosition = directive.lastIndex;
                                
                                if (res[2] != 'base')
                                    throw 'unknown directive "' + res[2] + '"';

                                stack[stack.length - 1].subnodes.push(new ast.BaseDirective({
                                    preDirectiveWhitespace : res[1],
                                    postDirectiveNameWhitespace : res[2],
                                    value : res[3],
                                    postValueWhitespace : res[4]
                                                }));

                                // TODO - record the base comp somehow

                                context = topLevelContext;                        
                                break;
                                
                            case suppressedLineBreakContext:

                                if (outputOn)
                                    stack[stack.length - 1].subnodes.push(new ast.SuppressedLineBreak());
                                else {
                                    var lastSubnode = stack[stack.length].lastSubnode();
                                    if (lastSubnode && lastSubnode instanceof ast.IgnoredContent)
                                        lastSubnode.setText(lastSubnode.text() + "\\\n");
                                    else
                                        stack[stack.length - 1].subnodes.push(new ast.IgnoredContent('\\\n'));
                                }
                                context = codeLineFollowingSuppressedLineBreak ?
                                    codeLineContext : inMethodContext;
                                codeLineFollowingSuppressedLineBreak = false;
                                break;

                            case outputStartContext:
                                
                                if (outputOn)
                                    throw 'found -| output start sequence where output already on';
                                outputOn = true;
                                stack[stack.length - 1].subnodes.push(new ast.OutputStart());
                                context = inMethodContext;
                                break;

                            case outputStopContext:
                                
                                if (! outputOn)
                                    throw 'found |- output stop sequence where output is already off';
                                outputOn = false;
                                stack[stack.length - 1].subnodes.push(new ast.OutputStop());
                                context = inMethodContext;
                                break;

                            case methodContext:

                                method.lastIndex = position;
                                res = method.exec(content);
                                if (res == null) throw 'malformed <~...> method tag';
                                newPosition = method.lastIndex;

                                methodNode = new ast.Method(res[1], res[2], res[3], res[4], res[5]);
                                stack[stack.length - 1].subnodes.push(methodNode);
                                stack.push(methodNode);
                                
                                context = inMethodContext;
                                break;
                                
                            case methodCloseContext:

                                methodClose.lastIndex = position;
                                res = methodClose.exec(content);
                                if (res == null) throw 'malformed </~...> closing method tag';
                                newPosition = methodClose.lastIndex;
                                
                                if (! stack[stack.length - 1] || ! (stack[stack.length - 1] instanceof ast.Method))
                                    throw '</~' + (res[2] || '') + '> without corresponding <~' + (res[2] || '') + '...> tag';
                                if (res[2] && stack[stack.length - 1].methodName != res[2])
                                    throw 'mismatched closing tag </~' + (res[2] || '') + '> for <~' + (stack[stack.length - 1].methodName || '') + '...> tag';

                                stack.pop();
                                if (stack.length == 1) {
                                    outputOn = true;
                                    context = topLevelContext;
                                }
                                else
                                    context = inMethodContext;
                                break;

                            case codeTagStartContext:

                                codeTagStart.lastIndex = position;
                                res = codeTagStart.exec(content);
                                if (res == null) throw 'cannot find end of code tag \']>\'';
                                newPosition = codeTagStart.lastIndex;

                                var lastSubnode = stack[stack.length - 1].lastSubnode();
                                if (lastSubnode && (lastSubnode instanceof ast.CodeTag))
                                    lastSubnode.code += res[1];
                                else
                                    stack[stack.length - 1].subnodes.push(new ast.CodeTag(res[1]));
                                context = inMethodContext;
                                break;

                            case expressionTagStartContext:

                                expressionTagStart.lastIndex = position;
                                res = expressionTagStart.exec(content);
                                if (res == null) throw 'cannot find end of expression tag \')...>\'';
                                newPosition = expressionTagStart.lastIndex;
                                if (res[1].search(/\S/) == -1)
                                    throw "empty expression tag";
                                var filters = [];
        /*                         var ftrs = res[2].split(/\,/); */
        /*                         for (var i = 0; i < ftrs.length; i++) { */
        /*                             if (this.filters[ftrs[i]]) { */
        /*                                 filters[filters.length] = new ast.UserFilter({ */
        /*                                     identifier : ftrs[i], */
        /*                                     method     : this.filters(ftrs[i]) */
        /*                                 }); */
        /*                             } */
        /*                             else if (builtinFilters[ftrs[i]]) { */
        /*                                 filters[filters.length] = new builtinFilters[ftrs[i]]({ */
        /*                                     identifier : ftrs[i] */
        /*                                 }); */
        /*                             } */
        /*                             else */
        /*                                 throw 'unknown filter \'' + ftrs[i] + '\' in expression tag'; */
        /*                         } */
                                stack[stack.length - 1].subnodes.push(new ast.ExpressionTag(res[1], filters));
                                context = inMethodContext;
                                break;
                                
                            case codeLineContext:

                                codeLine.lastIndex = position;

                                res = codeLine.exec(content);
                                if (res == null) throw 'cannot parse code line';
                                newPosition = codeLine.lastIndex;
                                
                                var lastSubnode = stack[stack.length - 1].lastSubnode();

                                if (lastSubnode && (lastSubnode instanceof ast.CodeLines))
                                    lastSubnode.code += res[1];
                                else
                                    stack[stack.length - 1].subnodes.push(new ast.CodeLines(res[1]));

                                if (content.charAt(newPosition) == ':') {
                                    newPosition++;
                                    break;
                                }
                                // why in method context here?
                                context = inMethodContext;
                                break;

                            case endOfInput:
                                if (! stack.shift())
                                    throw 'no root on container stack';
                                if (stack.length > 0) {
                                    throw stack.reverse().map(function (node) {
                                        if (! (node instanceof ast.Method))
                                            throw 'unknown node type: ' + node;
                                        
                                        return '<~' + (node.methodName || '') + '...> tag without corresponding </~> closing tag';
                                    }).join(', inside ');
                                }
                                break SUCCESS;

                            default:
                                throw 'unknown parse context: \'' + context + '\'';


                            }
                        }
                    }
                    catch (e) {
                        var lineRegex = /[^\n\r\f]*?(?:\015\012|\012|\015)/g;
                        var beginningOfErrorLine = 0;
                        lineRegex.lastIndex = 0;
                        var line = 1;
                        while (lineRegex.exec(content)) {
                            if (lineRegex.lastIndex <= tokenStart) {
                                line++;
                                beginningOfErrorLine = lineRegex.lastIndex;
                            }
                            else {
                                break;
                            }
                        }
                        //line += this.lineOffset;
                        var character = tokenStart - beginningOfErrorLine;
                        character++; // make it 1 based
                        throw e + " at line " + line + ", character " + character;
                    }
                    return root;
                }
            };
            return Parser;
		}();
	}
});

